﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

using Fuse8.DomainFramework.Common.Utilities;
using Fuse8.DomainFramework.Common;

namespace Fuse8.DomainFramework.DataAccess.SqlDatabase
{
	/// <summary>
	/// 
	/// </summary>
	public class SqlMap<T> where T : class, new()
	{
		#region Fields

		private static Dictionary<Type, SqlDbType> _typeMap = new Dictionary<Type, SqlDbType>();

		private static LRUCacheBag<TypeIdentity, Func<object, bool, List<SqlParameter>>> _cachedParamCreators = new LRUCacheBag<TypeIdentity, Func<object, bool, List<SqlParameter>>>();
        private static LRUCacheBag<TypeIdentity, Func<SqlDictionary, T, T>> _cachedSerializers = new LRUCacheBag<TypeIdentity, Func<SqlDictionary, T, T>>();

        private static MethodInfo _getValueMethod = typeof(SqlDictionary).GetMethod("get_Item", new Type[] { typeof(string) });
        private static MethodInfo _isDBNullMethod = typeof(SqlDictionary).GetMethod("IsDbNull", new Type[] { typeof(string) });

		private static MethodInfo _createMethod = typeof(SqlMap<T>).GetMethod("Create", BindingFlags.Static | BindingFlags.NonPublic);
		private static MethodInfo _addCollectionMethod = typeof(List<SqlParameter>).GetMethod("Add", BindingFlags.Instance | BindingFlags.Public);
	    private static MethodInfo _addColumn = typeof(DataColumnCollection).GetMethod("Add", new[] { typeof(string), typeof(Type) });
	    private static MethodInfo _getColumns = typeof(DataTable).GetProperty("Columns").GetGetMethod();
	    private static MethodInfo _addRow = typeof(DataRowCollection).GetMethod("Add", new[] { typeof(object[]) });
	    private static MethodInfo _getRows = typeof(DataTable).GetProperty("Rows").GetGetMethod();
	    private static MethodInfo _getEnumerator = typeof(IEnumerable).GetMethod("GetEnumerator");
	    private static MethodInfo _next = typeof(IEnumerator).GetMethod("MoveNext");
	    private static MethodInfo _getCurrent = typeof(IEnumerator).GetProperty("Current").GetGetMethod();
	    private static MethodInfo _getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle");

	    #endregion

		#region Properties

		internal IList<string> IgnoreToProperties { get; set; }

		internal IList<string> IgnoreFromProperties { get; set; }

		#endregion

		#region Constructor

		static SqlMap()
		{
			_typeMap[typeof(long)] = SqlDbType.BigInt;
			_typeMap[typeof(long?)] = SqlDbType.BigInt;

			_typeMap[typeof(byte[])] = SqlDbType.Image;

			_typeMap[typeof(bool)] = SqlDbType.Bit;
			_typeMap[typeof(bool?)] = SqlDbType.Bit;

			_typeMap[typeof(DateTime)] = SqlDbType.DateTime;
			_typeMap[typeof(DateTime?)] = SqlDbType.DateTime;

			_typeMap[typeof(decimal)] = SqlDbType.Decimal;
			_typeMap[typeof(decimal?)] = SqlDbType.Decimal;

			_typeMap[typeof(double)] = SqlDbType.Float;
			_typeMap[typeof(double?)] = SqlDbType.Float;

			_typeMap[typeof(int)] = SqlDbType.Int;
			_typeMap[typeof(int?)] = SqlDbType.Int;

			_typeMap[typeof(string)] = SqlDbType.NVarChar;

			_typeMap[typeof(Guid)] = SqlDbType.UniqueIdentifier;
			_typeMap[typeof(Guid?)] = SqlDbType.UniqueIdentifier;

			_typeMap[typeof(IEnumerable<>)] = SqlDbType.Structured;
		}

		#endregion

		#region Public methods

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sp"></param>
		/// <param name="parameters"></param>
		/// <param name="isForCreate"></param>
		/// <param name="spParameters"> </param>
		/// <returns></returns>
		public List<SqlParameter> GetParamInfo(string sp, object parameters, bool isForCreate, IList<SqlProcedureParameter> spParameters)
		{
			Guard.ArgumentNotEmpty(sp, "sp");

			List<SqlParameter> result = null;

			Func<object, bool, List<SqlParameter>> paramInfoGenerator;

			if (parameters != null)
			{
				TypeIdentity typeIdentity = new TypeIdentity(sp, parameters.GetType(), IgnoreToProperties, IgnoreFromProperties, isForCreate);
				lock (_cachedParamCreators)
				{
					if (!_cachedParamCreators.TryGetValue(typeIdentity, out paramInfoGenerator))
					{
						paramInfoGenerator = CreateParamInfoGenerator(typeIdentity, isForCreate, spParameters, sp);

						_cachedParamCreators.Add(typeIdentity, paramInfoGenerator);
					}
				}

				result = paramInfoGenerator(parameters, isForCreate);
			}

			return result;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sp"></param>
		/// <param name="reader"></param>
		/// <returns></returns>
		public T GetEntity(string sp, SqlDataReader reader)
		{
			Guard.ArgumentNotEmpty(sp, "sp");
			Guard.ArgumentNotNull(reader, "reader");

			var values = new SqlDictionary();

            for(int i=0;i<reader.FieldCount;i++)
            {
                values.Add(reader.GetName(i), reader.GetValue(i));
            }

            var deserializer = GetDeserializer(sp, values);

		    return deserializer(values, null);
		}

	    private Func<SqlDictionary, T, T> GetDeserializer(string sp, SqlDictionary values)
	    {
	        TypeIdentity typeIdentity = new TypeIdentity(sp, typeof (T), IgnoreToProperties, IgnoreFromProperties, false);
	        Func<SqlDictionary, T, T> deserializer;
	        lock (_cachedSerializers)
	        {
	            if (!_cachedSerializers.TryGetValue(typeIdentity, out deserializer))
	            {
	                deserializer = GetClassDeserializer(values, typeIdentity);

	                _cachedSerializers.Add(typeIdentity, deserializer);
	            }
	        }
	        return deserializer;
	    }

	    /// <summary>
	    /// 
	    /// </summary>
	    /// <param name="sp"></param>
	    /// <param name="entity"></param>
	    /// <param name="values"></param>
	    public void UpdateEntity(string sp, T entity, IDictionary<string, object> values)
        {
	        SqlDictionary sqlValues = new SqlDictionary(values);
	        var deserializer = GetDeserializer(sp, sqlValues);

            deserializer(sqlValues, entity);
        }

		#endregion

		#region Private methods

		private static Func<object, bool, List<SqlParameter>> CreateParamInfoGenerator(TypeIdentity typeIdentity, bool isForCreate, IList<SqlProcedureParameter> spParameters, string sp)
		{
			var resultType = typeof(List<SqlParameter>);

			DynamicMethod method = new DynamicMethod(
				string.Format("ParamInfo{0}", Guid.NewGuid().ToString()),
				resultType,
				new Type[] { typeof(object), typeof(bool) },
				true);

			var targetType = typeIdentity.Type;

			var query = typeof(T).Equals(targetType) ?
				targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
					.Where(p => p.CanRead && p.CanWrite)
					.OrderBy(p => p.Name) :
				targetType.GetProperties()
					.OrderBy(p => p.Name);

			var il = method.GetILGenerator();

			il.DeclareLocal(targetType);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Unbox_Any, targetType);
			il.Emit(OpCodes.Stloc_0);

			il.Emit(OpCodes.Newobj, resultType.GetConstructor(Type.EmptyTypes));

			foreach (var property in query)
			{
				//result.Add(SqlMap.Create("Id", System.Data.SqlDbType.Int, entity.Id, true));

				if ((typeIdentity.IgnoreToList != null) && typeIdentity.IgnoreToList.Contains(property.Name))
				{
					continue;
				}
			    bool isEnumerable = (property.PropertyType != typeof (string)) &&
			                        (property.PropertyType.GetInterface(typeof (IEnumerable).Name) != null);

				if (spParameters != null && !spParameters.Any(p => p.Name.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase)))
				{
					continue;
				}
                il.Emit(OpCodes.Dup);
                LocalBuilder localBuilder = null;
                if (isEnumerable)
                {
                    localBuilder = ConvertToDataTable(il, property, sp, spParameters.FirstOrDefault(p => p.Name.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase)));
                    //continue;
                }

				
				il.Emit(OpCodes.Ldstr, property.Name);
				il.Emit(OpCodes.Ldc_I4, (int)LookupDbType(property.PropertyType));
				
                if (!isEnumerable)
                {
                    il.Emit(OpCodes.Ldloc_0);
                    il.Emit(OpCodes.Callvirt, property.GetGetMethod());
                    if (property.PropertyType.IsValueType)
                    {
                        il.Emit(OpCodes.Box, property.PropertyType);
                    }
                }
                else
                {
                    il.Emit(OpCodes.Ldloc, localBuilder); 
                }

				if (isForCreate && property.Name.Equals("Id"))
				{
					il.Emit(OpCodes.Ldc_I4_1);
				}
				else
				{
					il.Emit(OpCodes.Ldc_I4_0);
				}

				il.Emit(OpCodes.Call, _createMethod);
				il.Emit(OpCodes.Callvirt, _addCollectionMethod);
			}

			il.Emit(OpCodes.Ret);

			return method.CreateDelegate(typeof(Func<object, bool, List<SqlParameter>>)) as Func<object, bool, List<SqlParameter>>;
		}

	    private static LocalBuilder ConvertToDataTable(ILGenerator il, PropertyInfo paramProperty, string sp, SqlProcedureParameter parameter)
	    {
	        if (parameter == null || parameter.Columns.Count == 0)
	        {
	            throw new NotSupportedException(string.Format(
	                "The parameter {0} is not supported by stored procedure {1}", paramProperty.Name, sp));
	        }
            var type = paramProperty.PropertyType;
            var entityType = type.GetGenericArguments()[0];

            var result = il.DeclareLocal(typeof(DataTable));
            var columns = il.DeclareLocal(typeof(DataColumnCollection));
            var column = il.DeclareLocal(typeof(DataColumn));
            // var result = new DataTable();
            il.Emit(OpCodes.Newobj, typeof(DataTable).GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Unbox_Any, typeof(DataTable));
            il.Emit(OpCodes.Stloc, result);
            il.Emit(OpCodes.Ldloc, result);           
            //var columns = result.Columns;
            il.Emit(OpCodes.Call, _getColumns);
            il.Emit(OpCodes.Unbox_Any, typeof(DataColumnCollection));
            il.Emit(OpCodes.Stloc, columns);
            IEnumerable<PropertyInfo> properties = entityType.GetProperties();
	        var isSimpleType = entityType.IsValueType || entityType == typeof(string);

	        foreach (var sqlColumn in parameter.Columns)
	        {
	            var info = properties.FirstOrDefault(p => p.Name == sqlColumn.SqlColumnName);
	            Type columnType = null;
                if (info != null)
                {
                    columnType = info.PropertyType;
                }
                else if(isSimpleType)
                {
                    columnType = entityType;
                }

                if (columnType != null)
                {
                    if (columnType.IsNullable())
                    {
                        columnType = columnType.GetNullableTypeParameter();
                    }

                    //var column = columns.Add(info.Name, info.PropertyType);
                    il.Emit(OpCodes.Ldloc, columns);
                    il.Emit(OpCodes.Ldstr, sqlColumn.SqlColumnName);
                    il.Emit(OpCodes.Ldtoken, columnType);
                    il.Emit(OpCodes.Call, _getTypeFromHandle);
                    il.Emit(OpCodes.Call, _addColumn);
                    il.Emit(OpCodes.Stloc, column);
                }
	        }

            var currentEl = il.DeclareLocal(entityType);
            var enumerator = il.DeclareLocal(typeof(IEnumerator));

            var rows = il.DeclareLocal(typeof(DataRowCollection));
            var row = il.DeclareLocal(typeof(DataRow));
            //var rows = result.Rows;
            il.Emit(OpCodes.Ldloc, result);
            il.Emit(OpCodes.Callvirt, _getRows);
            il.Emit(OpCodes.Unbox_Any, typeof(DataRowCollection));
            il.Emit(OpCodes.Stloc, rows);

            var stopLabel = il.DefineLabel();
            var startLabel = il.DefineLabel();
            //var enumerator = (parameter.get_ParameterProperty() as IEnumerable).GetEnumerator();
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Callvirt, paramProperty.GetGetMethod());
            il.Emit(OpCodes.Unbox_Any, typeof(IEnumerable));
            il.Emit(OpCodes.Callvirt, _getEnumerator);
            il.Emit(OpCodes.Stloc, enumerator);

            // while(enumerator.MoveNext())
            il.MarkLabel(startLabel);
            il.Emit(OpCodes.Ldloc, enumerator);
            il.Emit(OpCodes.Callvirt, _next);

            il.Emit(OpCodes.Brfalse, stopLabel);

            //currentEl = enumerator.Current as SomeType;
            il.Emit(OpCodes.Ldloc, enumerator);
            il.Emit(OpCodes.Callvirt, _getCurrent);
            il.Emit(OpCodes.Unbox_Any, entityType);
            il.Emit(OpCodes.Stloc, currentEl);

            var rowValues = il.DeclareLocal(typeof(object[]));
            //var rowValues = new object[SomeCount]
            il.Emit(OpCodes.Ldc_I4, parameter.Columns.Count);

            il.Emit(OpCodes.Newarr, typeof(object));
            il.Emit(OpCodes.Stloc, rowValues);
            int i = 0;
            foreach (var sqlColumn in parameter.Columns)
            {
                var info = properties.FirstOrDefault(p => p.Name == sqlColumn.SqlColumnName);
                Type columnType = null;
                if (info != null)
                {
                    columnType = info.PropertyType;
                }
                else if (isSimpleType)
                {
                    columnType = entityType;
                }

                if (columnType != null)
                {                    
                    il.Emit(OpCodes.Ldloc, rowValues);
                    il.Emit(OpCodes.Ldc_I4, i++);

                    if (!isSimpleType)
                    {
                        var prop = il.DeclareLocal(typeof(object));
                        il.Emit(OpCodes.Ldloc, currentEl);
                        il.Emit(OpCodes.Callvirt, info.GetGetMethod());
                        if (columnType.IsValueType)
                        {
                            il.Emit(OpCodes.Box, info.PropertyType);
                        }
                        il.Emit(OpCodes.Stloc, prop);
                        il.Emit(OpCodes.Ldloc, prop);
                    }
                    else
                    {
                        il.Emit(OpCodes.Ldloc, currentEl);
                        if (entityType.IsValueType)
                        {
                            il.Emit(OpCodes.Box, entityType);
                        }
                    }
                    il.Emit(OpCodes.Stelem_Ref);
                }
            }

	        //rows.Add(rowValues);
            il.Emit(OpCodes.Ldloc, rows);
            il.Emit(OpCodes.Ldloc, rowValues);
            il.Emit(OpCodes.Callvirt, _addRow);
            il.Emit(OpCodes.Stloc, row);
            
            il.Emit(OpCodes.Br, startLabel);
            //end while
            il.MarkLabel(stopLabel);

	        return result;
	    }

	    internal static Func<SqlDictionary, T, T> GetClassDeserializer(SqlDictionary reader, TypeIdentity typeIdentity)
		{
			var genType = typeof(T);

			DynamicMethod method = new DynamicMethod(
				string.Format("Deserialize{0}", Guid.NewGuid().ToString()),
				genType,
                new Type[] { typeof(SqlDictionary), typeof(T) },
				true);

			var il = method.GetILGenerator();

			var properties = genType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
				.Select(
					p => new
					{
						Name = p.Name,
						Setter = p.GetSetMethod(),
						Type = p.PropertyType
					})
				.Where(p => p.Setter != null);

			var names = new List<string>();
			foreach (var key in reader.Keys)
			{
				names.Add(key);
			}

			var setters = names.Select(
				p => new
				{
					Name = p,
					Info = properties.FirstOrDefault(q => q.Name == p)
				});

			Label finishLabel;
            
			LocalBuilder result = il.DeclareLocal(typeof(T));
            var nullLabel = il.DefineLabel();
            var setLabel = il.DefineLabel();
            il.Emit(OpCodes.Ldarg_1);
            // kind of if (entity != null)
            il.Emit(OpCodes.Brfalse, nullLabel);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Stloc, result);
            il.MarkLabel(nullLabel);
            // kind of else
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Brtrue, setLabel);
            il.Emit(OpCodes.Newobj, genType.GetConstructor(Type.EmptyTypes));                                          
			il.Emit(OpCodes.Stloc, result);
            il.MarkLabel(setLabel);

			foreach (var item in setters)
			{
				if (!((typeIdentity.IgnoreFromList != null) && typeIdentity.IgnoreFromList.Contains(item.Name)))
				{
					if (item.Info != null)
					{
						finishLabel = il.DefineLabel();
                        
						il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldstr, item.Name);
						
						il.Emit(OpCodes.Callvirt, _isDBNullMethod);
						il.Emit(OpCodes.Brtrue, finishLabel);

						il.Emit(OpCodes.Ldloc, result);
						il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldstr, item.Name);
						il.Emit(OpCodes.Callvirt, _getValueMethod);

						il.Emit(OpCodes.Unbox_Any, item.Info.Type);
						il.Emit(OpCodes.Callvirt, item.Info.Setter);

						il.MarkLabel(finishLabel);
					}
				}				
			}
			il.Emit(OpCodes.Ldloc, result);
			il.Emit(OpCodes.Ret);

            return method.CreateDelegate(typeof(Func<SqlDictionary, T, T>)) as Func<SqlDictionary, T, T>;
		}

		private static void EmitInt32(ILGenerator il, int value)
		{
			switch (value)
			{
				case -1: il.Emit(OpCodes.Ldc_I4_M1);
					break;
				case 0: il.Emit(OpCodes.Ldc_I4_0);
					break;
				case 1: il.Emit(OpCodes.Ldc_I4_1);
					break;
				case 2: il.Emit(OpCodes.Ldc_I4_2);
					break;
				case 3: il.Emit(OpCodes.Ldc_I4_3);
					break;
				case 4: il.Emit(OpCodes.Ldc_I4_4);
					break;
				case 5: il.Emit(OpCodes.Ldc_I4_5);
					break;
				case 6: il.Emit(OpCodes.Ldc_I4_6);
					break;
				case 7: il.Emit(OpCodes.Ldc_I4_7);
					break;
				case 8: il.Emit(OpCodes.Ldc_I4_8);
					break;
				default: il.Emit(OpCodes.Ldc_I4, value);
					break;
			}
		}

		private static SqlDbType LookupDbType(Type type)
		{
			SqlDbType result;

		    var key = !(type.IsEnum || type.IsNullableEnum()) ? type : typeof(int);
            if (type.GetInterface(typeof(IEnumerable<>).Name) != null && type != typeof(string))
            {
                key = typeof (IEnumerable<>);
            }
		    if (!_typeMap.TryGetValue(key, out result))
			{
				throw new NotSupportedException(string.Format("The type : {0} is not supported by the mapper", type));
			}

			return result;
		}

        //used via reflection
		private static SqlParameter Create(string name, SqlDbType type, object val, bool isForCreate)
		{
			var sqlParameter = new SqlParameter(string.Format("@{0}", name), type);

			sqlParameter.Direction = isForCreate ?
				type == SqlDbType.Int ?
					ParameterDirection.ReturnValue :
					ParameterDirection.InputOutput :
				ParameterDirection.Input;

			sqlParameter.Value = val ?? DBNull.Value;

			if (type == SqlDbType.NVarChar)
			{
				sqlParameter.Size = -1;
			}

			return sqlParameter;
		}

		#endregion
	}

	internal class TypeIdentity : IEquatable<TypeIdentity>
	{
		#region Fields

		private int _hashCode;
		private RuntimeTypeHandle _typeHandle;

		#endregion

		#region Properties

		public string TypeName
		{
			get;
			private set;
		}

		public IList<string> IgnoreToList
		{
			get;
			private set;
		}

		public IList<string> IgnoreFromList
		{
			get;
			private set;
		}

		public bool IsForCreate
		{
			get;
			private set;
		}

		public string Sp
		{
			get;
			private set;
		}

		public Type Type
		{
			get
			{
				return Type.GetTypeFromHandle(_typeHandle);
			}
		}

		#endregion

		#region Constructor

		public TypeIdentity(string sp, Type type, IList<string> ignoreToList, IList<string> ignoreFromList, bool isForCreate)
		{
			Sp = sp;

			_typeHandle = type.TypeHandle;

			TypeName = type.Name;
			IgnoreToList = ignoreToList;
			IgnoreFromList = ignoreFromList;
			IsForCreate = isForCreate;

			string hashCodeField = string.Format("{0}.{1}.{2}", Sp, TypeName, IsForCreate);
			if (ignoreToList != null)
			{
				StringBuilder sb = new StringBuilder();
				foreach (var item in ignoreToList)
				{
					sb.AppendFormat("{0}", item);
				}

				hashCodeField = string.Format("{0}.{1}", hashCodeField, sb.ToString());
			}
			if (ignoreFromList != null)
			{
				StringBuilder sb = new StringBuilder();
				foreach (var item in ignoreFromList)
				{
					sb.AppendFormat("{0}", item);
				}

				hashCodeField = string.Format("{0}.{1}", hashCodeField, sb.ToString());
			}

			_hashCode = hashCodeField.GetHashCode();
		}

		#endregion

		#region Methods

		public override bool Equals(object obj)
		{
			return Equals(obj as TypeIdentity);
		}

		public override int GetHashCode()
		{
			return _hashCode;
		}

		#endregion

		#region IEquatable<TypeIdentity> Members

		public bool Equals(TypeIdentity other)
		{
			return other != null ?
				other.GetHashCode() == GetHashCode() :
				false;
		}

		#endregion
	}

    internal class SqlDictionary : Dictionary<string, object>
    {
        public SqlDictionary(IDictionary<string, object> dictionary) : base(dictionary)
        {
        }

        public SqlDictionary():base()
        {
            
        }

        public bool IsDbNull(string name)
        {
            return Convert.IsDBNull(this[name]);
        }
    }
}
